2019 Year in Review

2019前端体系及核心知识点总结

深冻结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let person = {
name: 'aaa',
lesson: {
name: 'english',
type: 'basic'
}
};

function deepFreeze(obj) {
let propNames = Object.getOwnPropertyNames(obj);

for (let name of propNames) {
let value = obj[name];

obj[name] = value && typeof value === 'object' ? deepFreeze(value) : value;
}

return Object.freeze(obj);
}

deepFreeze(person);

person.lesson.name = 123;

防抖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
debounce(fn, delay) {
let timer = null;

return (...args) => {
if (timer) {
clearTimeout(timer);
timer = null;
} else {
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
}
};
}

节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 定时器实现
throttle(fn, delay) {
let timer = null;

return (...args) => {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
clearTimeout(timer);
timer = null;
}, delay);
}
};
}

// 时间戳实现
throttle(fn, delay) {
let startTime = Date.now();

return (...args) => {
let curTime = Date.now();

if (curTime - startTime >= delay) {
fn.apply(this, args);
startTime = Date.now();
}
};
}

// 时间戳 + 定时器实现
// 保证第一次触发事件就能立即执行事件处理函数和每隔 delay 时间执行一次事件处理函数
throttle(fn, delay) {
let timer = null;
let startTime = Date.now();

return (...args) => {
let curTime = Date.now();
let remainTime = delay - (curTime - startTime);

clearTimeout(timer);

if (remainTime <= 0) {
fn.apply(this, args);
startTime = Date.now();
} else {
timer = setTimeout(fn, remainTime);
}
};
}

可存储函数执行结果的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
memeorize(fn) {
let cache = {};

return (...args) => {
const key = args.toString();

if (cache[key]) {
return cache[key];
}

let value = fn.apply(this, args);
cache[key] = value;

return value;
};
}

将函数转化为 Promise 的使用

1
2
3
4
5
6
7
8
9
10
11
promisy(fn) {
return (...args) => {
return new Promise((resolve, reject) => {
try {
fn(...args, resolve);
} catch (e) {
reject(e);
}
});
};
}

函数柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术
参数复用、提前返回和延迟执行

1
2
3
4
5
6
7
// Tips:伪数组转真数组
// Array.prototype.slice.call({
// length: 2,
// '0': '123',
// '1': 'aaa'
// });
// let argsArr = [].slice.call(arguments);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
currying(fn) {
let allArgs = [];

function next () {
let args = Array.prototype.slice.call(arguments);

allArgs = [...allArgs, ...args];

return next;
};

// 字符类型
next.toString = function() {
return fn.apply(null, allArgs);
};

// 数值类型
next.valueOf = function() {
return fn.apply(null, allArgs);
}

return next;
}

currying(fun) {
function helper(fn, ...arg1) {
let length = fn.length;
let self = this;

return function(...arg2) {
let arg = arg1.concat(arg2);

if (arg.length < length) {
return helper.call(self, fn, ...arg);
} else {
return fn.apply(this, arg);
}
};
}

return helper(fun);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 认识真相的五大阻力:
// 从众心理
// 认知偏见
// 情绪干扰
// 思维局限
// 逻辑谬误

// 选择、时机和运气

// BEM
// OOCSS
// OOSASS
// SMACSS

// p-页面(Page)(应用于body元素的类)对可维护性不是那么重要的静态页面十分有用 —应该避免嵌套使用 (例: p-Homepage);
// l-布局(Layout), 比如列(columning),包裹(wrappers) 和容器(containers)等等(例: l-Masthead, l-Footer);
// c-组件(components )(例: c-Dropdown, c-Button…);
// u-公共类(Utility classes) — 不会发生改变, 在代码的任何地方都不能重载。(例: u-textCenter, u-clearfix…);
// js-JavaScript钩子:永远不应该出现在CSS中;
// g-JavaScript钩子:全局js类,永远不应该出现在CSS中;

// Serverless = FaaS + BaaS
// 事件驱动:函数在 FaaS 平台中,需要通过一系列的事件来驱动函数执行。
// 无状态:因为每次函数执行,可能使用的都是不同的容器,无法进行内存或数据共享。如果要共享数据,则只能通过第三方服务,比如 Redis 等。
// 无运维:使用 Serverless 我们不需要关心服务器,不需要关心运维。这也是 Serverless 思想的核心。
// 低成本:使用 Serverless 成本很低,因为我们只需要为每次函数的运行付费。函数不运行,则不花钱,也不会浪费服务器资源
// 存在问题:标准不统一

// SSR解决:白屏时间和SEO
// SSR问题:运维成本
// SSR路由 -> Serverless函数
// https://pic2.zhimg.com/80/v2-937eabff94a6c53b2d53d105332dc001_hd.jpg

// 运用场景:小程序云开发

// 函数测试:
// 1、将业务逻辑和函数依赖的 FaaS 和 BaaS 分离
// 2、对业务逻辑进行充分的单元测试
// 3、将函数进行集成测试验证代码是否正常工作

// 函数性能:(优化冷启动时间)
// 函数生命周期:https://pic4.zhimg.com/80/v2-c440550443505856d11efa6efe86d623_hd.jpg
// 1、选用 Node.js / Python 等冷启动时间短的编程语言
// 2、为函数分配合适的运行内存 函数不是每次都冷启动,而是会在一定时间内复用之前的运行环境!
// 3、执行上下文重用
// 4、给函数“预热”(隔一段时间就冷启动一个运行环境)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
// https://juejin.im/post/5cef46226fb9a07eaf2b7516
1.
const isType = type => target => `[object ${type}]` === Object.prototype.toString.call(target)

const selfIsArray = isType('Array')

Array.selfIsArray || (Object.defineProperty(Array, 'selfIsArray', {
value: selfIsArray,
enumerable: false,
configurable: true,
writable: false
}))

console.log(selfIsArray([])) // true

2.
// 循环实现map
const selfMap = function(fn, context) {
let arr = Array.prototype.slice.call(this)
let mappedArr = [];

for (let i = 0, len = arr.length; i < len; i++) {
// 判断稀疏数组的情况
if (!arr.hasOwnProperty(i)) continue;
mappedArr[i] = fn.call(context, arr[i], i, this)
}

return mappedArr
}

// reduce实现map
// 由于 reduce 会跳过空单元数组,所以这个 polyfill 无法处理空单元数组
const selfMap2 = function(fn, context) {
let arr = Array.prototype.slice.call(this)

return arr.reduce((pre, cur, index) => [...pre, fn.call(context, cur, index, this)], [])
}

Array.prototype.selfMap || (Object.defineProperty(Array.prototype, 'selfMap', {
value: selfMap,
enumerable: false,
configurable: true,
writable: true
}))

Array.prototype.selfMap2 || (Object.defineProperty(Array.prototype, 'selfMap2', {
value: selfMap2,
enumerable: false,
configurable: true,
writable: true
}))

let arr = ['z', 'h', 'l']
console.log(arr.selfMap(item => item + '1'))

3.
// 循环实现filter
const selfFilter = function(fn, context) {
let arr = Array.prototype.slice.call(this)
let filteredArr = []

for (let i = 0, len = arr.length; i < len; i++) {
if (!arr.hasOwnProperty(i)) continue
fn.call(context, arr[i], i, this) && filteredArr.push(arr[i])
}

return filteredArr
}

// reduce实现filter
const selfFilter2 = function(fn, context) {
return this.reduce((pre, cur, index) => {
return fn.call(context, cur, index, this) ? [...pre, cur] : [...pre]
}, [])
}

Array.prototype.selfFilter || (Object.defineProperty(Array.prototype, 'selfFilter', {
value: selfFilter,
enumerable: false,
configurable: true,
writable: true
}))

Array.prototype.selfFilter2 || (Object.defineProperty(Array.prototype, 'selfFilter2', {
value: selfFilter2,
enumerable: false,
configurable: true,
writable: true
}))

let arr = [1, 2, 3]
console.log(arr.selfFilter(item => item === 2))
// 当给 filter 函数传入第二个参数时,第一个参数不能为箭头函数,否则为词法绑定,第二个参数会失效,其他迭代方法同理
console.log(arr.selfFilter2(function(item) {
return item === 2
}, ['a', 'b', 'c']))

4.
// ES5循环实现 some 方法
const selfSome = function (fn, context) {
let arr = Array.prototype.slice.call(this)
// 空数组直接返回 false,数组的 every 方法则相反返回 true
if(!arr.length) return false
for (let i = 0; i < arr.length; i++) {
if(!arr.hasOwnProperty(i)) continue;
let res = fn.call(context,arr[i],i,this)
if(res)return true
}
return false
}

Array.prototype.selfSome ||(Object.defineProperty(Array.prototype, 'selfSome', {
value: selfSome,
enumerable: false,
configurable: true,
writable: true
}))

let arr = [1, 2, 3, 4, 5]
console.log(arr.selfSome(item => item === 2))

5.
// ES5循环实现reduce
Array.prototype.selfReduce = function (fn, initialValue) {
let arr = Array.prototype.slice.call(this)
let res
let startIndex
if (initialValue === undefined) {
// 找到第一个非空单元(真实)的元素和下标
for (let i = 0; i < arr.length; i++) {
if (!arr.hasOwnProperty(i)) continue
startIndex = i
res = arr[i]
break
}
} else {
res = initialValue
}
// 遍历的起点为上一步中找到的真实元素后面一个真实元素
// 每次遍历会跳过空单元的元素
for (let i = ++startIndex || 0; i < arr.length; i++) {
if (!arr.hasOwnProperty(i)) continue
res = fn.call(null, res, arr[i], i, this)
}
return res
}

Array.prototype.selfReduce || (Object.defineProperty(Array.prototype, 'selfReduce', {
value: selfReduce,
enumerable: false,
configurable: true,
writable: true
}))

let arr = [1, 2, 3, 4, 5]
console.log(arr.selfReduce((acc, cur) => acc + cur))
console.log(arr.reduce((acc, cur) => acc + cur))

6.
// reduce实现 Array.prototype.flat,数组扁平化
const selfFlat = function (depth = 1) {
let arr = Array.prototype.slice.call(this)
if (depth === 0) return arr
return arr.reduce((pre, cur) => {
if (Array.isArray(cur)) {
// 需要用 call 绑定 this 值,否则会指向 window
return [...pre, ...selfFlat.call(cur, depth - 1)]
} else {
return [...pre, cur]
}
}, [])
}

Array.prototype.selfFlat || (Object.defineProperty(Array.prototype, 'selfFlat', {
value: selfFlat,
enumerable: false,
configurable: true,
writable: true
}))

let arr = [1, 2, [3, 4, [5, 6, 7, 8], 9], 10, 11, 12, [13, 14]]
console.log(arr.selfFlat()) // 传入 Inifity 会将传入的数组变成一个一维数组

7.
// 寄生组合式继承 + 构造函数之间的继承
function inherit(subType, superType) {
// 由于JavaScript引用类型和函数按值传递的特性,不能改变 subType 的引用地址
subType.prototype = Object.create(superType.prototype, {
constructor: {
enumerable: false,
configurable: true,
writable: true,
// 指向子类,和默认的继承行为保持一致
value: subType
}
})

//子构造函数继承父构造函数(子类继承父类的静态方法和静态属性)
Object.setPrototypeOf(subType, superType)
}

8.
https://github.com/yeyan1996/JavaScript/blob/master/curry.js

9.
/**
* @description 偏函数(创建已经设置好一个或多个参数的函数,并且添加了占位符功能)
* @param {Function} func -部分求值的函数
* @param {...*} [args] -部分求值的参数
* @return {Function} -部分求值后的函数
**/

const partialFunc = (func, ...args) => {
let placeholderNum = 0
return (...args2) => {
args2.forEach(arg => {
let index = args.findIndex(item => item === "_")
if (index < 0) return
args[index] = arg
placeholderNum++
})
if (placeholderNum < args2.length) {
args2 = args2.slice(placeholderNum, args2.length)
}
return func.apply(this, [...args, ...args2])
}
}

const display = (a, b, c, d, e, f, g, h) => [a, b, c, d, e, f, g, h];

let partialDisplay = partialFunc(display, 1, 2)
console.log("partialFunc", partialDisplay(3, 4, 5, 6, 7, 8))

let partialDisplay2 = partialFunc(display, '_', 2, '_') // 使用占位符
console.log('partialFunc2', partialDisplay2(1, 3, 4, 5, 6, 7, 8))

10.
const speed = function (fn, num) {
console.time('time')
let value = fn(num)
console.timeEnd('time')
console.log(`返回值:${value}`)
}

/**
* @description 斐波那契数列
* @param {number} n -第几个位置
* @return {number} 参数对应在数列中的数字
**/
let fibonacci = function (n) {
if (n < 1) return 0
if (n === 1 || n === 2) return 1
return fibonacci(n - 1) + fibonacci(n - 2)
}
speed(fibonacci, 35) // 简单递归实现 缺点:达到一定数量级时变的很慢 耗性能


// 函数记忆
const memory = function (fn) {
let obj = {}
return function (n) {
if (obj[n] === undefined) obj[n] = fn(n)
return obj[n]
}
}
fibonacci = memory(fibonacci)
speed(fibonacci, 35) // 利用闭包特性

// 动态规划方案
function fibonacci_DP_1(n) {
// 用数组来存取每一次产生子问题的结果
if (n <= 0) return 0
if (n === 1) return 1
var arr = [0, 1]
for (var i = 2; i <= n; i++) {
arr[i] = arr[i - 1] + arr[i - 2]
}
return arr[n]
}

function fibonacci_DP_2(n) {
// 局部变量来实现可以省下内存空间
if (n <= 0) return 0
if (n === 1) return 1
var res, a = 0, b = 1
for (var i = 2; i <= n; i++) {
res = a + b;
a = b;
b = res;
}
return res
}

function fibonacci_DP_3(n) {
// 最优解版本
if (n <= 0) return 0
if (n <= 1) return 1
var a = 0, b = 1
for (var i = 2; i <= n; i++) {
b = a + b
a = b - a
}
return b
}
speed(fibonacci_DP_3, 35)

11.
// 求最长公共子串的动态规划算法

12.
// 0-1背包问题

13.
// 实现函数 bind 方法
// 会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数
const isComplexDataType = obj => obj !== null && (typeof obj === 'function' || typeof obj === 'object')

const selfBind = function (bindTarget, ...args1) {
if (typeof this !== 'function') throw new TypeError('Bind must be called on a function')

const originFunc = this
const boundFunc = function (...args2) {
// 使用 new 关键字调用返回新对象
if (new.target) {
const res = originFunc.call(this, ...args1, ...args2)
// 如果构造函数返回一个对象则返回这个对象
if (isComplexDataType(res)) return res
// 否则返回新建的对象
return this
} else {
originFunc.call(bindTarget, ...args1, ...args2)
}
}

// 真正的 bind 创建的函数是没有 prototype 的,取而代之有个 [[TargetFunction]] 保存 bind 前的函数
// 使用 new 会将创建的对象的 __proto__ 连接 [[TargetFunction] prototype (非箭头函数)
// 这里给 bind 后的函数手动设置一个 prototype 属性,模拟这个行为
// 箭头函数则没有 prototype
if (originFunc.prototype) boundFunc.prototype = originFunc.prototype

// 定义绑定后函数的长度和名字
const desc = Object.getOwnPropertyDescriptors(originFunc)
Object.defineProperties(boundFunc, {
length: desc.length,
name: Object.assign(desc.name, {
value: `bound ${desc.name.value}`
})
})

return boundFunc
}

14.
// 实现函数 call 方法
// call 核心:
// 1、将函数设置为对象的属性
// 2、执行&删除这个函数
// 3、指定 this 到函数并传入给定参数执行函数
// 4、如果不传入参数,默认指向为 window
Function.prototype.selfCall = function (context = window, ...args) {
let func = this // 首先要获取调用call的函数,用this可以获取
if (typeof func !== 'function') throw new TypeError('this is not function')
let caller = Symbol('caller')
context[caller] = func // 创建一个caller属性,值设置为需要调用的函数
let res = context[caller](...args)
delete context[caller]
return res
}

let foo = {
value: 1
}

function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}

bar.selfCall(foo, 'black', '18') // black 18 1

// 实现函数 apply 方法
Function.prototype.selfApply = function (context = window) {
if (typeof this !== 'function') throw new TypeError('this is not function')
context.fn = this
let res
// 判断是否有第二个参数
if (arguments[1]) {
res = context.fn(...arguments[1])
} else {
res = context.fn()
}
delete context.fn
return res
}

15.
// 简易的 CO 模块

16.
// 函数防抖
// 需要处理3个问题:
// 1、this 指向
// 2、event 对象
// 3、返回值
// 4、是否立刻执行
const debounce = (fn, delay = 1000, options = {
leading: true, // 是否立刻执行
context: null
}) => {
let timer;

const _debounce = function (...args) {
if (timer) clearTimeout(timer)

if (options.leading && !timer) {
timer = setTimeout(null, delay)
fn.apply(options.context, args)
} else {
timer = setTimeout(() => {
fn.apply(options.context, args)
timer = null
}, delay)
}
}

_debounce.cancel = function () {
clearTimeout(timer)
timer = null
}

return _debounce
}

17.
// 函数节流
// leading 和 trailing 无法同时为 false
const throttle = (fn, delay = 1000, options = {
leading: true, // 是否立刻执行
trailing: false, // 是否在最后额外触发一次
context: null
}) => {
let timer;
let startTime = Date.now();

let _throttle = function (...args) {
let curTime = Date.now()

if (!options.leading) {
if (timer) return
timer = setTimeout(() => {
fn.apply(options.context, args)
timer = null
}, delay)
} else if (curTime - startTime >= delay) {
fn.apply(options.context, args)
startTime = curTime
} else if (options.trailing) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(options.context, args)
}, delay)
}
}

// 闭包返回取消函数
_throttle.cancel = function () {
startTime = 0
clearTimeout(timer)
timer = null
}

return _throttle
}

// * 使用 Proxy 实现函数节流
function proxy (func, time, options = {
leading: false,
trailing: true,
context: null
}) {
let timer;
let previous = new Date(0).getTime();

let handler = {
apply(target, _, args) {
// 和闭包实现核心逻辑相同
let now = new Date().getTime();

if (!options.leading) {
if (timer) return;
timer = setTimeout(() => {
timer = null;
Reflect.apply(func, options.context, args)
}, time)
} else if (now - previous > time) {
Reflect.apply(func, options.context, args)
previous = now
} else if (options.trailing) {
clearTimeout(timer)
timer = setTimeout(() => {
Reflect.apply(func, options.context, args)
}, time)
}
}
};

return new Proxy(func, handler)
}

18.
// 图片懒加载
// getBoundingClientRect() 实现
let imgList = [...document.querySelectorAll('img')]

let lazyLoad = (function () {
let count = 0

return function () {
let deleteIndexList = []

imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()

if (rect.top < window.innerHeight) {
// 进入视口
img.src = img.dataset.src
// 加载成功后将图片添加到删除列表中
deleteIndexList.push(index)
count++
if (count === imgList.length) document.removeEventListener('scroll', lazyLoad)
}
})

// 删除已经加载完毕的图片
imgList = imgList.filter((_, index) => !deleteIndexList.includes(index))
}
})()

lazyLoad = proxy(lazyLoad, 1000)

lazyLoad() // 手动加载一次,否则首屏的图片不触发滚动无法加载
document.addEventListener('scroll', lazyLoad)


// IntersectionObserver 实现
let lazyLoad = function () {
let observer = new IntersectionObserver(entries => {
// entries 存储着所有被观察元素的 intersectionObserverEntry 配置
entries.forEach(entry => {
// 大于0表示进入视口
if (entry.intersectionRatio > 0) {
entry.target.src = entry.target.dataset.src
// 取消观察
observer.unobserve(entry.target)
}
})
})

imgList.forEach(img => {
observer.observe(img)
})
}

lazyLoad()

19.
// new 关键字
// 1、创建了一个全新的对象
// 2、使 this 指向新创建的对象
// 3、它会被执行[[Prototype]]链接
// 4、通过 new 创建的每个对象将最终被 [[Prototype]] 链接到这个函数的原型 prototype 对象上
// 5、如果函数没有返回对象类型 Object (包含Functoin, Array, Date, RegExg, Error),那么 new 表达式中的函数调用将返回该对象引用
const selfNew = function (fn, ...args) {
let instance = Object.create(fn.prototype) // __proto__
let res = fn.call(instance, ...args)
return isComplexDataType(res) ? res : instance
}

function Person (name, sex) {
this.name = name
this.sex = sex
}

console.log(new Person('ma', 'male'))
// equals to
console.log(selfNew(Person, 'ma', 'male'))

20.
// 实现 Object.assign
"use strict" //启用严格模式在尝试给基本包装类型已定义的下标赋值的时候报错

const selfAssign = function (target, ...source) {
if (target == null) throw new TypeError('Cannot convert undefined or null to object')

return source.reduce((acc, cur) => {
isComplexDataType(acc) || (acc = new Object(acc)); // 变成一个基本包装类型

if (cur == null) return acc; // source 为 null, undefined 时忽略

// 遍历出 Symbol 属性和可枚举属性
[...Object.keys(cur), ...Object.getOwnPropertySymbols(cur)].forEach(key => {
acc[key] = cur[key]
})

return acc
}, target)
}

Object.selfAssign || Object.defineProperty(Object, 'selfAssign', {
value: selfAssign,
configurable: true,
enumerable: false,
writable: false
})

let target = {
a: 1,
b: 1
}

let obj1 = {
a: 2,
b: 2,
c: undefined
}

let obj2 = {
a: 3,
b: 3,
[Symbol("a")]: 3,
d: null
}

console.log(Object.selfAssign(target, obj1, obj2))
console.log(Object.selfAssign("abd", null, undefined))

21.
// instanceof
const selfInstanceof = function (left, right) {
let proto = Object.getPrototypeOf(left)

while (true) {
if (proto == null) return false
if (proto === right.prototype) return true
proto = Object.getPrototypeOf(proto)
}
}

console.log(selfInstanceof({}, Array))

22.
// 洗牌算法
// 通过洗牌算法可以达到真正的乱序,洗牌算法分为原地和非原地
// 原地的洗牌算法,不需要声明额外的数组从而更加节约内存占用率,原理是依次遍历数组的元素,将当前元素和之后的所有元素中随机选取一个进行交换
function shuffle (arr) {
for (let i = 0; i < arr.length; i++) {
let randomIndex = i + Math.floor(Math.random() * (arr.length - i))
[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]]
}
return arr
}

// 新生成一个数组,随机从原数组中取出一个元素放入新数组
function shuffle2 (arr) {
let _arr = []
while (arr.length) {
let randomIndex = Math.floor(Math.random() * (arr.length))
_arr.push(arr.splice(randomIndex, 1)[0])
}
return _arr
}

23.
// 单例模式
// 通过 ES6 的 Proxy 拦截构造函数的执行方法来实现的单例模式
function proxy (func) {
let instance;
let handler = {
construct(target, args) {
if (!instance) {
// 没有实例就创造一个实例
instance = Reflect.construct(func,args)
}
// 无论如何都会返回一个实例(new关键字)
return instance
}
}

return new Proxy(func, handler)
}

function Person(name, age) {
this.name = name
this.age = age
}

const SingletonPerson = proxy(Person)

let person1 = new SingletonPerson('zhl', 22)

let person2 = new SingletonPerson('cyw', 22)

console.log(person1 === person2) // true

24.
// promisify
// 使用 nodejs 运行以下代码
// 适合err-first风格的异步操作(eg. nodejs)的 promisify 通用函数
const fs = require("fs")

function promisify(asyncFunc) {
return function (...args) {
return new Promise((resolve, reject) => {
args.push(function callback(err, ...values) {
if (err) return reject(err)
return resolve(...values)
});

asyncFunc.call(this, ...args);
});
};
}

const fsp = new Proxy(fs, {
get(target, key) {
return promisify(target[key])
}
})

async function generateCommit() {
try {
let data = await fsp.readFile('./promisify.js', 'utf-8')
data += `\n//我是注释`
await fsp.writeFile('./promisify.js', data)
} catch (e) {
console.log(e)
}
}

generateCommit()

25.
// async/await 优雅处理方式
async function errorCaptured(asyncFunc) {
try {
let res = await asyncFunc()
return [null,res]
} catch (e) {
return [e,null]
}
}

26.
// 发布订阅EventEmitter
class EventEmitter {
constructor() {
this.subs = {}
}

on(event, cb) {
(this.subs[event] || (this.subs[event] = [])).push(cb)
}

trigger(event, ...args) {
this.subs[event] && this.subs[event].forEach(cb => {
// cb.apply(this, ...args)
cb(...args)
})
}

once(event, onceCb) {
const cb = (...args) {
onceCb(...args)
this.off(event, onceCb)
}

this.on(event, cb)
}

off(event, offCb) {
if (this.subs[event]) {
const idx = this.subs[event].findIndex(cb => cb === offCb)
this.subs[event].splice(idx, 1)
if (!this.subs[event].length) delete this.subs[event]
}
}
}

let dep = new EventEmitter()

let cb = function () {
console.log('handleClick')
}

let cb2 = function () {
console.log('handleMouseover')
}

console.group()
dep.on('click', cb)
dep.on('click',cb2)
dep.trigger('click')
console.groupEnd()

console.group()
dep.off('click', cb)
dep.trigger('click')
console.groupEnd()

console.group()
dep.once('mouseover', cb2)
dep.trigger('mouseover')
dep.trigger('mouseover')
console.groupEnd()

27.
// 简单实现 JSON.stringify 方法
const isString = value => typeof value === 'string'
const isSymbol = value => typeof value === 'symbol'
const isUndefined = value => typeof value === 'undefined'
const isDate = obj => Object.prototype.toString.call(obj) === '[object Date]'
const isFunction = obj => Object.prototype.toString.call(obj) === '[object Function]'
const isComplexDataType = value => (typeof value === 'object' || typeof value === 'function') && value !== null
const isValidBasicDataType = value => value !== undefined && !isSymbol(value) // 合法的基础类型
const isValidObj = obj => Array.isArray(obj) || Object.prototype.toString.call(obj) === '[object Object]' // 合法的复杂类型(对象)
const isInfinity = value => value === Infinity || value === -Infinity

// 在数组中存在 Symbol/Undefined/Function/Infinity/NaN 类型会变成 null
const processSpecialValueInArray = value =>
isSymbol(value) || isFunction(value) || isUndefined(value) || isInfinity(value) || isNaN(value) ? null : value

// 根据 JSON 规范处理属性值
const processValue = value => {
if (isInfinity(value) || isNaN(value)) {
return null
}
if (isString(value)) {
return `"${value}"`
}

return value
}

let s = Symbol('s')
let obj = {
str: "123",
arr: [1, { e: 1 }, s, () => {}, undefined, Infinity, NaN],
obj: { a: 1 },
Infinity: -Infinity,
nan: NaN,
undef: undefined,
symbol: s,
date: new Date(),
reg: /123/g,
func: () => {},
dom: document.querySelector('body')
};

obj.loop = obj // 一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略!!!

const jsonStringify = (function () {
// 闭包 + WeakMap 防止循环引用
let wp = new WeakMap()

// 递归调用 jsonStringify 的都是闭包中的这个函数,而非 const 声明的 jsonStringify 函数
return function jsonStringify(obj) {
if (wp.get(obj)) throw new TypeError('Converting circular structure to JSON')
let res = ""

if (isComplexDataType(obj)) {
// 复杂类型的情况
if (obj.toJSON) return obj.toJSON; // 含有 toJSON 方法则直接调用
if (!isValidObj(obj)) return // 非法的复杂类型直接返回

wp.set(obj, obj);

if (Array.isArray(obj)) {
// 数组的情况
res += "[";
let temp = []; //声明一个临时数组用来控制属性之间的逗号
obj.forEach((value) => {
temp.push(isComplexDataType(value) && !isFunction(value) ? jsonStringify(value) : `${processSpecialValueInArray(value, true)}`)
});
res += `${temp.join(',')}]`
} else {
// 对象的情况
res += "{";
let temp = [];
Object.keys(obj).forEach((key) => {
// 值是对象的情况
if (isComplexDataType(obj[key])) {
// 值是合法对象的情况
if (isValidObj(obj[key])) {
temp.push(`"${key}":${jsonStringify(obj[key])}`)
} else if (isDate(obj[key])) { // Date 类型调用 toISOString
temp.push(`"${key}":"${obj[key].toISOString()}"`)
} else if (!isFunction(obj[key])) { // 其余非函数类型返回空对象
temp.push(`"${key}":{}`)
}
} else if (isValidBasicDataType(obj[key])) {
// 值是基本类型
temp.push(`"${key}":${processValue(obj[key])}`)
}
});
res += `${temp.join(',')}}`
}
} else if (isSymbol(obj)) {
// Symbol 返回 undefined
return
} else {
// 非 Symbol 的基本类型直接返回
return obj
}

return res
}
})();

console.log(jsonStringify(obj))
console.log(JSON.stringify(obj))

28.
// JSON.parse 实现
1、eval 会执行JS代码,有XSS漏洞
2、new Function 与 eval 有相同的字符串参数特性
3、递归
4、状态机(比如正则引擎、词法分析,甚至是字符串匹配的 KMP 算法都能用它来解释。它代表着一种本质的逻辑:在 A 状态下,如果输入 B,就会转移到 C 状态)
5、script(模拟jsonP的方式拼接字符串然后以callBack的方式返回)
// https://github.com/youngwind/blog/issues/115
// var jsonStr = '{ "age": 20, "name": "jack" }'
// var json = (new Function(jsonStr))();
// {age: 20, name: "jack"}

29.
// 手写一个符合 Promise/A+ 规范的 Promise
// https://github.com/forthealllight/blog/issues/4
// https://github.com/xieranmaya/blog/issues/2
// 1、三种必须状态 pending | fulfilled(resolved) | rejected
// 当处于 pending 状态的时候,可以转移到 fulfilled(resolved) 或者 rejected 状态
// 当处于 fulfilled(resolved) 状态或者 rejected 状态的时候,就不可变
// 2、一个promise必须有一个then方法,then方法接受两个参数:promise.then(onFulfilled, onRejected)
// onFulfilled方法表示状态从pending——>fulfilled(resolved)时所执行的方法,而onRejected表示状态从pending——>rejected所执行的方法
// 3、为了实现链式调用,then方法必须返回一个promise:promise = promise.then(onFulfilled, onRejected)
function Promise (excutor) {
let that = this; // 缓存当前 promise 实例对象
that.status = 'pending'; // 初始状态
that.value = undefined; // fulfilled 状态时 返回的信息 if pending -> fulfilled
that.reason = undefined; // rejected 状态时 拒绝的原因 if pending -> rejected
that.onFulfilledCallbacks = []; // 存储 fulfilled 状态对应的 onFulfilled 函数, to deal with async(resolved)
that.onRejectedCallbacks = []; // 存储 rejected 状态对应的 onRejected 函数, to deal with async(rejeced)

// value 成功态时接收的终值
// pending -> fulfilled
function resolve (value) {
if(value instanceof Promise) {
return value.then(resolve, reject);
}
// 实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
setTimeout(() => {
// 调用 resolve 回调对应 onFulfilled 函数
if (that.status === 'pending') {
// 只能由 pending 状态 => fulfilled 状态 (避免调用多次 resolve reject)
that.status = 'fulfilled';
that.value = value;
that.onFulfilledCallbacks.forEach(cb => cb(that.value)); // 遍历回调数组并执行
}
}); // delay 默认值为0,DOM_MIN_TIMEOUT_VALUE是4ms
}

// reason失败态时接收的拒因
// pending -> rejected
function reject (reason) {
setTimeout(() => {
// 调用 reject 回调对应 onRejected 函数
if (that.status === 'pending') {
// 只能由 pending 状态 => rejected状态 (避免调用多次resolve reject)
that.status = 'rejected';
that.reason = reason;
that.onRejectedCallbacks.forEach(cb => cb(that.reason));
}
});
}

try {
excutor(resolve, reject); // According to the definition that the function "constructor" accept two parameters
} catch (e) {
reject(e); // error catch 执行函数过程中会遇到错误,需要捕获错误并且执行 reject 函数
}
}

Promise.prototype.then = function (onFulfilled, onRejected) {
const that = this;
// 每个 then 函数都需要返回一个新的 Promise 对象
let newPromise;
// 处理参数默认值 保证参数后续能够继续执行
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason };

if (that.status === 'pending') {
// 等待态
// 当异步调用 resolve/reject 时 将 onFulfilled/onRejected 收集暂存到集合中
newPromise = new Promise((resolve, reject) => {
that.onFulfilledCallbacks.push(() => {
try {
let x = onFulfilled(that.value); // 规范规定
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});

that.onRejectedCallbacks.push(() => {
try {
let x = onRejected(that.reason);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
});
}

if (that.status === 'fulfilled') {
// 成功态(resolved)
newPromise = new Promise((resolve, reject) => {
// 规范规定:传入的函数的函数体需要异步执行
// Promise/A+ 规范有说Promise.then的回调执行必须是异步的,实现方式是多种多样的,可以选择Macro Task机制,也可以选择Micro Task机制,这边用的setTimeout就是Macro Task机制
setTimeout(() => {
try{
let x = onFulfilled(that.value);
resolvePromise(newPromise, x, resolve, reject); // 新的 promise resolve 上一个 onFulfilled 的返回值
} catch(e) {
reject(e); // 捕获前面 onFulfilled 中抛出的异常 then(onFulfilled, onRejected);
}
});
})
}

if (that.status === 'rejected') {
// 失败态
newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(that.reason);
resolvePromise(newPromise, x, resolve, reject);
} catch(e) {
reject(e);
}
});
});
}

return newPromise
};

// 实现兼容多种 Promise
// Tips:
// resolvePromise 函数,该函数接受当前的 promise、onFullfilled 函数或者 onRejected 函数的返回值 x、resolve 和 reject 作为参数
// 处理 then 函数中的 onFullfilled 和 onRejected 方法的返回值问题
// then函数的返回值 ——> 返回一个新promise,从而实现链式调用
// then函数中的 onFullfilled 和 onRejected 方法 ——> 返回基本值或者新的promise
function resolvePromise (promise, x, resolve, reject) {
// promise must !== x
if (promise === x) {
// 造成循环引用
return reject(new TypeError("TypeError: Cyclic reference"));
}

let isCalled = false; // 是否已经调用过函数

if (x !== null && (typeof x === "object" || typeof x === "function")) {
try {
let then = x.then;

if (typeof then === "function") {
// 如果 then 是函数类型的话,就将 x 作为函数的作用域 this 调用之,并且传递两个回调函数作为参数,第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise,两个回调函数都需要判断是否已经执行过函数,然后进行相应的逻辑
then.call(
x,
y => {
if (isCalled) return;
isCalled = true;
resolvePromise(promise, y, resolve, reject)
},
e => {
if (isCalled) return;
isCalled = true;
reject(e);
})
} else {
resolve(x); // 仅仅是一个函数或者是对象
}
} catch (e) {
if (isCalled) return;
isCalled = true;
reject(e);
}
} else {
resolve(x); // 返回的基本类型,直接resolve
}
}

export default Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
前端2018
// 1. 基础扎实,对主流技术栈十分了解
// 2. 技术发展兼顾前后端
// 3. 职业素养更加综合
// 4. 资历背景优秀
// 对原理的理解
// 优秀前端的核心竞争力其实就是「折腾」
// ES6的所有新特性
// webpack Parcel
// PWA
// webapck4
// 前端工程化

前端2019
// React / Vue
// WebAssembly
// TypeScript
// React hooks
// CSS-in-JS
// Vue 3.x
// Flutter
// Next / Nuxt
// GraphQL
// Deno
// Docker,Kubernetes

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Primitive/Object
* typeof 对于原始类型来说,除了 null 都可以显示正确的类型
* typeof 对于对象来说,除了函数都会显示 object
* instanceof 内部机制是通过原型链来判断的
* foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)
* Symbol.toPrimitive 该方法在转原始类型时调用优先级最高
* 比较运算符: 如果是对象,就通过 toPrimitive 转换对象; 如果是字符串,就通过 unicode 字符索引来比较
* 箭头函数其实是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this
* bind 这些改变上下文的 API 了,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window
* 不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定
* this 的规则:new 的方式优先级最高,接下来是 bind 这些函数,然后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变
* Function 的 toString,返回的是定义函数时,等号右边的全部内容字符串
* 非基础数据类型的对象,在遇到 “+” 操作时,都是先调用自己的 toString 方法,自己没有就找原型上的,得到结果之后再拼接。只是每种对象默认的 toString 方法实现不一样而已
* 如果a,b计算值中有对象,那么把对象调用 [Symbol.toPrimitive]转换成基础类型,具体在转换过程中是先调用 valueOf还是 ToString,是要看传给 [Symbol.toPrimitive]的参数,对于 + 号操作符,是优先调用 valueOf,然后 ToString
* 1.'如果另一个操作符为Array、number等,则将操作符通过toString()转为字符串进行拼接' 转换成字符串的优先级是 [Symbol.toString]> valueOf > toString,并不仅仅是 toString
2.整个流程应该是
1)如果左右两个操作数有一个是对象,那么先将对象按照 [Symbol.toString]> valueOf > toString来转换成基本数据类型
2)转换后的两个操作数类型中如果Sstring,那么把另外不是String的操作数转换成字符串按照字符串拼接
3)转换后的两个操作数类型中没有String,那么把左右操作数都转换数字进行计算
* 值类型 == 值类型 // 相当于 Number(值类型) == Number(值类型)
引用类型 == 值类型 // 对象转化成原始类型的值,再进行比较。比较规则:数组与数值进行比较,会先转成数值,再进行比较;与字符串进行比较,会先转成字符串,再进行比较;与布尔值进行比较,两个运算子都会先转成数值,然后再进行比较。
[] == [] // false []属于引用类型,在两个[]分别指向不同的堆内存
[] == ![] // true ![]为false -> [] == false -> ToPrimitive([]) == false -> '' == false -> 0 == 0 -> true
' \t\r\n ' == 0 // true 转义字符 Number(' \t\r\n ') -> 0
* 浅拷贝
直接赋值
Object.assign (当object只有一层的时候,是深拷贝)
Array.prototype.concat()
Array.prototype.slice()
展开运算符 ...
深拷贝
// 1、JSON安全
var newObj = JSON.parse(JSON.stringify(someObj));
问题:
1.无法实现对函数 、RegExp等特殊对象的克隆
2.会抛弃对象的constructor,所有的构造函数会指向Object
3.对象有循环引用,会报错
4.忽略undefined和symbol

// 2、复杂数据类型:递归&遍历(考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等)
// https://lodash.com/docs#cloneDeep
deepClone() {

}
* 原型
// https://note.youdao.com/ynoteshare1/index.html?id=a4b93ec64df3c32ef7a7c9fa2da64c20&type=note
// https://image-static.segmentfault.com/167/246/1672461057-58204120a29cc_articlex
Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
函数的 prototype 是一个对象
对象的 __proto__ 属性指向原型, __proto__ 将对象和原型连接起来组成了原型链

ES6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
* hoisting: 提升的是声明
* 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
* let 和 const 声明变量,变量并不会被挂载到 window 上
* 寄生组合继承:
function Parent(value) {
this.value = value
}
Parent.prototype.getValue = function() {
console.log(this.value)
}

function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
configurable: true,
writable: true
}
})

const child = new Child(123)

child.getValue() // 123
child instanceof Parent // true
* 类继承
// class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)
class Person {
constructor(f, l) {
this.f = f;
this.l = l;
}

personMethod() {
console.log(this.f)
}
}

class Employee extends Person {
constructor(f, l, position) {
super(f, l);
this.position = position;
}

employeeMethod() {
// ...
}
}

let employee = new Employee(123)
employee.personMethod() // 123
* 模块化
1、立即执行函数
2、AMD 和 CMD
// AMD
define(['./a', './b'], function(a, b) {
// 加载模块完毕可以使用
a.do()
b.do()
})
// CMD
define(function(require, exports, module) {
// 加载模块
// 可以把 require 写在函数体的任意地方实现延迟加载
var a = require('./a')
a.doSomething()
})
3、CommonJS(就是包装了一层立即执行函数)
4、ES module
// ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别
CommonJS 支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案
CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
ES Module 会编译成 require/exports 来执行的
// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function() {}
5、Proxy
// let p = new Proxy(target, handler) target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数
* map
// 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中
// map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组
* filter
// 生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素
* reduce
// arr.reduce(callback(accumulator, currentValue, currentIndex, array), initialValue)
// 回调函数第一次执行时,accumulator 和currentValue的取值有两种情况:如果调用reduce()时提供了initialValue,accumulator取值为initialValue,currentValue取数组中的第一个值;如果没有提供 initialValue,那么accumulator取数组中的第一个值,currentValue取数组中的第二个值
// 可以将数组中的元素通过回调函数最终转换为一个值
// 接受两个参数,分别是回调函数和初始值
const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)
// 首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入
// 回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数
// 在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入
// 所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6
  • 并发(concurrency)和并行(parallelism)
    // 并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
    // 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。
  • Callback (Callback hell)
    // 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
    // 嵌套函数一多,就很难处理错误
    1.Generator
    // 控制函数的执行
    // 返回一个迭代器
    // 可以通过 Generator 函数解决回调地狱的问题
    
    2.Promise
    // 很好地解决了回调地狱的问题
    // 构造 Promise 的时候,构造函数内部的代码是立即执行的
    // Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装
    // 无法取消 Promise,错误需要通过回调函数捕获
    
    3.async 及 await
    // 一个函数如果加上 async ,那么该函数就会返回一个 Promise
    // async 就是将函数返回值使用 Promise.resolve() 包裹了下,和 then 中处理返回值一样,并且 await 只能配套 async 使用
    // await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator,会让出线程,阻塞后面的代码先执行async外的同步代码
    // 如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果
    // 优雅地解决回调地狱问题
    // 缺点:await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低
    
  • setTimeout、setInterval、requestAnimationFrame
    // JS 是单线程执行的,如果前面的代码影响了性能,就会导致 setTimeout 不会按期执行
    // requestAnimationFrame 自带函数节流功能,基本可以保证在 16.6 毫秒内只执行一次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题
    // 定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码
  • Event Loop
    // https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
    // 进程与线程:进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。线程是进程中的更小单位,描述了执行一段指令所需的时间
    // 执行栈:存储函数调用的栈结构,遵循先进后出的原则,执行 JS 代码的时候其实就是往执行栈中放入函数,遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为
    // 在程序的整个生命周期中不停的重复 主任务 ——> micro task ——> 渲染视图 ——> macro task 的操作
    // 微任务会在当前宏任务的同步代码执行完毕,才会依次执行
    1.浏览器中

    // 首先执行同步代码,这属于宏任务
    // 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
    // 执行所有微任务
    // 当执行完所有微任务后,如有必要会渲染页面
    // 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数
    
    // 不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task
    // 宏任务包括:Events,Parsing,Callbacks,Using a resource,Reacting to DOM manipulation,script,setTimeout,setInterval,setImmediate,I/O,UI rendering
    // 微任务包括:process.nextTick,promise,MutationObserver,其中 process.nextTick 为 Node 独有
    

    2.Node中

    // 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段
    2.1 macrotask
      timer // 执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的
      I/O // 处理一些上一轮循环中的少数未执行的 I/O 回调
      idle,prepare
      poll // 回到 timer 阶段执行回调;执行 I/O 回调
      check // 执行 setImmediate
      close callbacks // 执行 close 事件
    2.2 microtask
      Timers
      IO Callbacks
      IO Polling
      Set Immediate
      Close Events
    
  • process.nextTick
    // 它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行
  • 事件触发有三个阶段
    1.window 往事件触发处传播,遇到注册的捕获事件会触发
    2.传播到事件触发处时触发注册的事件
    3.从事件触发处往 window 传播,遇到注册的冒泡事件会触发
    // 使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 false ,useCapture 决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性:
    1.capture:布尔值,和 useCapture 作用一样
    2.once:布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听
    3.passive:布尔值,表示永远不会调用 preventDefault
    
    // stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件
    // stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件
    // 事件代理的方式相较于直接给目标注册事件:节省内存、不需要给子节点注销事件
  • 跨域同源策略:主要是用来防止 CSRF 攻击(利用用户的登录态发起恶意请求)
    // 请求发出去了,被浏览器拦截了响应
    1.JSONP

    function jsonp(url, jsonpCallback, success) {
      let script = document.createElement('script')
      script.src = url
      script.async = true
      script.type = 'text/javascript'
      window[jsonpCallback] = function(data) {
        success && success(data)
      }
      document.body.appendChild(script)
    }
    
    jsonp('http://xxx', 'callback', function(value) {
      console.log(value)
    })
    

    2.CORS

    // 服务端设置 Access-Control-Allow-Origin 就可以开启 CORS
    // 简单请求
    // 复杂请求:预检请求 OPTIONS
    

    3.document.domain

    // 用于二级域名相同的情况
    

    4.postMessage

    // 用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息
    // 发送消息端
    window.parent.postMessage('message', 'http://test.com')
    // 接收消息端
    var mc = new MessageChannel()
    mc.addEventListener('message', event => {
      var origin = event.origin || event.originalEvent.origin
      if (origin === 'http://test.com') {
        console.log('验证通过')
      }
    })
    

    5.nginx反向代理

  • cookie
    // http-only 不能通过 JS 访问 Cookie,减少 XSS 攻击
    // secure 只能在协议为 HTTPS 的请求中携带
    // same-site 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击
  • Service Worker
    // 请求拦截和结果缓存
    // 独立线程,网络代理,一般可以用来实现缓存功能
    // 传输协议必须为 HTTPS,涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全
    1.注册 Service Worker
    2.监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据
  • Web Worker
    // 开辟线程执行耗时js
  • 浏览器缓存机制
    缓存位置
    1.Service Worker
    2.Memory Cache
      // 对于大文件来说,大概率是不存储在内存中的,反之优先
      // 当前系统内存使用率高的话,文件优先存储进硬盘
    3.Disk Cache
      // 容量和存储时效性
      // 即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据
    4.Push Cache
      // HTTP/2
      // 缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放
      // 可以推送 no-cache 和 no-store 的资源
      // 一旦连接被关闭,Push Cache 就被释放
      // 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存
      // Push Cache 中的缓存只能被使用一次
      // 浏览器可以拒绝接受已经存在的资源推送
      // 你可以给其他域名推送资源
    5.网络请求
    
    缓存策略
    // 强缓存和协商缓存
    // 缓存策略都是通过设置 HTTP Header 来实现
    1.强缓存
      // Expires 和 Cache-Control
      // 表示在缓存期间不需要请求,state code 为 200
      // Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效
      // Cache-Control 优先级高于 Expires, max-age=30 该属性值表示资源会在 30 秒后过期,需要再次请求
    2.协商缓存
      // Last-Modified 和 ETag
      Last-Modified 和 If-Modified-Since
        // Last-Modified 表示本地文件最后修改日期
        // If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码
        弊端:
        // 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
        // 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
      ETag 和 If-None-Match
        // ETag 优先级比 Last-Modified 高
    // 未设置缓存策略:启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间
    
  • 浏览器渲染原理
    1.解析 HTML:Byte Data -> String -> Token -> Node -> DOM Tree
    2.解析 CSS:Byte Data -> String -> Token -> Node -> CSSOM
    3.Render Tree:DOM Tree + CSSOM
    4.当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上

    Tip:插入几万个 DOM,如何实现页面不卡顿?
    // requestAnimationFrame
    // 虚拟滚动(virtualized scroller):只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容

    // defer:立即下载,但延迟执行
    // async:JS 文件下载和解析不会阻塞渲染

    // 关键渲染路径

  • 安全
    1.XSS 分持久型和非持久型
    // 转义字符
    // 白名单
    // CSP
      // 设置 HTTP Header 中的 Content-Security-Policy
      // 设置 meta 标签的方式 <meta http-equiv="Content-Security-Policy">
    
    2.CSRF
    // SameSite
    // 验证 Referer
    // Token
    
    3.点击劫持
    // X-FRAME-OPTIONS 一个 HTTP 响应头,防御用 iframe 嵌套的点击劫持攻击
      // DENY,表示页面不允许通过 iframe 的方式展示
      // SAMEORIGIN,表示页面可以在相同域名下通过 iframe 的方式展示
      // ALLOW-FROM,表示页面可以在指定来源的 iframe 中展示
    
    4.中间人攻击
    // 中间人攻击是攻击方同时与服务端和客户端建立起了连接,并让对方认为连接是安全的,但是实际上整个通信过程都被攻击者控制了
    
  • 性能优化
    // 图片优化
    //
    //
    //
    // 懒执行
    // 懒加载
    // CDN
  • Webpack 性能优化
    1. 减少 Webpack 打包时间
      1.1 优化 Loader
      // 优化 Loader 的文件搜索范围:include、exclude
      // 将 Babel 编译过的文件缓存起来:loader: ‘babel-loader?cacheDirectory=true’
      1.2 HappyPack
      // HappyPack 可以将 Loader 的同步执行转换为并行的
      module: {
      loaders: [
      {
        test: /\.js$/,
        include: [resolve('src')],
        exclude: /node_modules/,
        // id 后面的内容对应下面
        loader: 'happypack/loader?id=happybabel'
      }
      
      ]
      },
      plugins: [
      new HappyPack({
      id: 'happybabel',
      loaders: ['babel-loader?cacheDirectory'],
      // 开启 4 个线程
      threads: 4
      
      })
      ]
      1.3 DllPlugin
      // DllPlugin 可以将特定的类库提前打包然后引入
      // webpack.DllPlugin()
      // webpack.DllReferencePlugin()
      1.4 代码压缩
      // Webpack3: webpack-parallel-uglify-plugin 来并行运行 UglifyJS
      // Webpack4: mode设置production
    2. 减少 Webpack 打包后的文件体积
      2.1 按需加载
      // 每个路由页面单独打包为一个文件
      2.2 Scope Hoisting
      // 分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去
      // Webpack4: optimization.concatenateModules = true
      2.3 Tree Shaking
      // 实现删除项目中未被引用的代码
      2.4 开启gzip压缩
  • 自己实现一个Webpack打包工具?
    // https://www.youtube.com/watch?v=Gc9-7PBqOC8&list=LLHK1mTHpwrUeYgF5gu-Kd4g
    // https://github.com/ronami/minipack/blob/master/src/minipack.js
  • MVC or MVVM
    // MVC 有一个巨大的缺陷就是控制器承担的责任太大了,随着项目愈加复杂,控制器中的代码会越来越臃肿,导致出现不利于维护的情况
    // 通过 ViewModel 将视图中的状态和用户的行为分离出一个抽象
  • Virtual DOM
    // 对比完两棵树以后,就可以通过差异去局部更新 DOM,实现性能的最优化
    1.将 Virtual DOM 作为一个兼容层,让我们还能对接非 Web 端的系统,实现跨端开发
    2.通过 Virtual DOM 我们可以渲染到其他的平台,比如实现 SSR、同构渲染等
    3.实现组件的高度抽象化
  • 前端路由
    // 监听 URL 的变化,然后匹配路由规则,显示相应的页面,并且无须刷新页面
    1.Hash 模式

    // 更简单,并且兼容性也更好
    window.addEventListener('hashchange', () => {
    
    })
    

    2.History 模式

    // history.pushState 和 history.replaceState
    // 新增历史记录 history.pushState(stateObject, title, URL)
    // 替换当前历史记录 history.replaceState(stateObject, title, URL)
    // 点击后退按钮时会触发 popState 事件
    window.addEventListener('popstate', e => {
      // e.state 就是 pushState(stateObject) 中的 stateObject
      console.log(e.state)
    })
    
  • Vue

  • 组件通信
    1.父子通信
    // 单向数据流
    // props 传递数据给子组件,子组件不能直接修改 props,必须通过发送事件的方式告知父组件修改数据
    // $attrs 属性透传
    // emit 发送事件传递数据给父组件
    // v-mode 语法糖 默认会解析成名为 value 的 prop 和名为 input 的事件
    // 通过访问 $parent 或者 $children 对象来访问组件实例中的方法和数据
    // $listeners 属性会将父组件中的 (不含 .native 修饰器的) v-on 事件监听器传递给子组件,子组件可以通过访问 $listeners 来自定义监听器
    // .sync 属性是个语法糖
    <!--父组件中-->
    <input :value.sync="value" />
    <!--以上写法等同于-->
    <input :value="value" @update:value="v => value = v"></comp>
    <!--子组件中-->
    <script>
      this.$emit('update:value', 1)
    </script>
    
    2.兄弟组件通信
    // 通过查找父组件中的子组件实现 this.$parent.$children
    
    3.跨多层次组件通信
    // provide / inject:Vue 2.2 新增的 API
    
    4.任意组件
    // Vuex
    // Event Bus
    
  • Vue.extend()
    // 扩展组件生成一个构造器,通常会与 $mount 一起使用
  • mixin & mixins
    // mixin 用于全局混入,会影响到每个组件实例
    // mixins 扩展组件的方式,上拉下拉加载数据这种逻辑(先于组件内的钩子函数执行)
  • computed & watch
    // computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容
    // watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作
    // computed 和 watch 还都支持对象的写法
  • keep-alive
    // 保存一些组件的状态防止多次渲染
    // 拥有两个独有的生命周期钩子函数,分别为 activated 和 deactivated
    // 缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数
  • $attrs
    // 属性透传,使用起来也比较简单,避免了多写 props
  • 响应式原理
    1.Object.defineProperty() getter/setter 劫持了数据的读写操作,使我们可以在数据读写时得到通知并执行自定义的操作
    2.递归遍历
    3.订阅/发布: Dep 和 Watcher
    4.数组的特殊响应化处理 Object.create(Array.prototype)
    5.精准依赖收集:为每个键都维护一个 Dep
    6.异步更新队列:避免不必要的计算和 DOM 操作(MutationObserver 与 Promise.then 中的回调均在下一个 microTask 中执行)
    7.key 的添加与删除(每个 key 对应值为对象/数组创建了另一个 dep,挂在该对象和数组的 ob 属性上,setter 操作,通知 set/get 中的 dep;非 setter 操作,如对象 key 添加删除、数组变异方法调用,通知 ob 中的 dep)
    8.深度数据追踪:递归进去遍历每一个子属性,主动触发一下 getter
    9.Proxy:支持监测数组的 push 等方法操作,支持对象属性的动态添加和删除,极大的简化了响应化的代码量
    // 依赖收集:在 getter 过程中标记当前的键,仅在被标记的键修改时,才去触发订阅的更新(在键的 getter 触发时将当前 watcher 加入 dep 中,完成依赖收集)
    // 简单实现:data-reactivity-system-demo.js
    // 通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作,更精确的来说,对于数组而言,大部分操作都是拦截不到的
  • 编译过程
    1.将模板解析为 AST
    // 基本的 AST 对象
    {
      // 类型
      type: 1,
      // 标签
      tag,
      // 属性列表
      attrsList: attrs,
      // 属性映射
      attrsMap: makeAttrsMap(attrs),
      // 父节点
      parent,
      // 子节点
      children: []
    }
    // 根据这个最基本的 AST 对象中的属性,进一步扩展 AST
    
    2.优化 AST
    // 静态内容提取
    // 提取静态的属性
    
    3.将 AST 转换为 render 函数
    // 历整个 AST,根据不同的条件生成不同的代码
    
  • nextTick 原理分析
    // 本质是 microtask!!
    // MutationObserver 和 Promise.then(Vue 2.0.0-rc.7曾尝试用window.postMessage,是macrotask,不可行)
    // Vue.nextTick和Vue.prototype.$nextTick都是直接使用了这个nextTick
    // 在batcher中,也就是watcher观测到数据变化后执行的是nextTick(flushBatcherQueue),flushBatcherQueue则负责执行完成所有的dom更新操作
    // nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM
    // 在新版本中,会默认使用 microtasks,但在特殊情况下会使用 macrotasks,比如 v-on
    // 实现 macrotasks:会先判断是否能使用 setImmediate ,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout
    // 最新版的Vue源码里,优先使用Promise.resolve().then(nextTickHandler)来将异步回调放入到microtask中(MO在IOS9.3以上的WebView中有bug),没有原生Promise才用MO
    if (typeof setImmediate !== ‘undefined’ && isNative(setImmediate)) {

    macroTimerFunc = () => {
      setImmediate(flushCallbacks)
    }
    

    } else if (

    typeof MessageChannel !== 'undefined' &&
    (isNative(MessageChannel) ||
      // PhantomJS
      MessageChannel.toString() === '[object MessageChannelConstructor]')
    

    ) {

    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = flushCallbacks
    macroTimerFunc = () => {
      port.postMessage(1)
    }
    

    } else {

    macroTimerFunc = () => {
      setTimeout(flushCallbacks, 0)
    }
    

    }
    // https://github.com/Ma63d/vue-analysis/issues/6

  • React
    // Fiber 机制(V16)
    // 对于异步渲染,现在渲染有两个阶段:reconciliation 和 commit 。前者过程是可以打断的,后者不能暂停,会一直更新界面直到完成。
    Reconciliation 阶段

    1.componentWillMount
    2.componentWillReceiveProps -> getDerivedStateFromProps
    3.shouldComponentUpdate
    4.componentWillUpdate -> getSnapshotBeforeUpdate
    

    Commit 阶段

    1.componentDidMount
    2.componentDidUpdate
    3.componentWillUnmount
    

    setState
    // 是个异步 API,多次调用放入一个队列中,在恰当的时候统一进行更新过程
    // 如果想实时:
    this.setState((prevState) => ({ count: prevState.count + 1 }), () => {

    console.log(this.state)
    

    })

    性能优化
    // shouldComponentUpdate 函数中我们可以通过返回布尔值来决定当前组件是否需要更新。这层代码逻辑可以是简单地浅比较一下当前 state 和之前的 state 是否相同,也可以是判断某个值更新了才触发组件更新
    // React.PureComponent
    // React.memo()

    通信
    1.父子通信

    // 父组件通过 props 传递数据给子组件,子组件通过调用父组件传来的函数传递数据给父组件
    // 单向数据流
    

    2.兄弟组件通信

    // 通过共同的父组件来管理状态和事件函数
    

    3.跨多层级组件通信

    // React.createContext()
    

    4.任意组件

    // Redux
    // Event Bus
    

    高阶组件(HOC)

    事件机制
    // 合成事件(SyntheticEvent)

    Hooks
    // 抽离重复逻辑 不会增加组件的嵌套 实现状态的共享
    // 通过函数组件的方式去管理状态,并且也能将四散的业务逻辑写成一个个 Hooks 便于复用以及维护
    useState
    useEffect
    useRef
    useCallback

  • 监控
    页面埋点
    性能监控
    // performance.getEntriesByType('navigation')
    // https://www.jianshu.com/p/47a6b7866ba6
    
    异常监控
  • UDP
    1.面向无连接
    // 数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作
    // 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
    // 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
    
    2.不可靠性
    // 某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP
    
    3.高效
    // UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的
    
    4.传输方式
    // UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能
    
  • HTTP/2
    1.二进制传输
    2.多路复用 帧(frame)和流(stream)
    3.Header 压缩
    4.服务端 Push
    5.
  • HTTP/3
    QUIC
    多路复用
    0-RTT
    纠错机制
    
    // HTTP/2 通过多路复用、二进制流、Header 压缩等等技术,极大地提高了性能,但是还是存在着问题的
    // QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
* 设计模式
工厂模式:隐藏了创建实例的复杂度,只需要提供一个接口
单例模式:保证全局只有一个对象可以访问
适配器模式:用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作
装饰模式:不需要改变已有的接口,作用是给对象添加功能
代理模式:控制对对象的访问,不让外部直接访问到对象
发布/订阅模式(观察者模式):通过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知
外观模式:提供了一个接口,隐藏了内部的逻辑,更加方便外部调用
策略模式:
* 数据结构
栈:线性结构,只能在某一端添加或删除数据,遵循先进后出的原则
队列:线性结构,特点是在某一端添加数据,在另一端删除数据,遵循先进先出的原则
单链队列:
class Queue {
constructor() {
this.queue = []
}
enQueue(item) {
this.queue.push(item)
}
deQueue() {
return this.queue.shift()
}
getHeader() {
return this.queue[0]
}
getLength() {
return this.queue.length
}
isEmpty() {
return this.getLength() === 0
}
}
循环队列:单链队列在出队操作的时候需要 O(n) 的时间复杂度,所以引入了循环队列。循环队列的出队操作平均是 O(1) 的时间复杂度
class SqQueue {
constructor(length) {
this.queue = new Array(length + 1)
// 队头
this.first = 0
// 队尾
this.last = 0
// 当前队列大小
this.size = 0
}
enQueue(item) {
// 判断队尾 + 1 是否为队头
// 如果是就代表需要扩容数组
// % this.queue.length 是为了防止数组越界
if (this.first === (this.last + 1) % this.queue.length) {
this.resize(this.getLength() * 2 + 1)
}
this.queue[this.last] = item
this.size++
this.last = (this.last + 1) % this.queue.length
}
deQueue() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
let r = this.queue[this.first]
this.queue[this.first] = null
this.first = (this.first + 1) % this.queue.length
this.size--
// 判断当前队列大小是否过小
// 为了保证不浪费空间,在队列空间等于总长度四分之一时
// 且不为 2 时缩小总长度为当前的一半
if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
this.resize(this.getLength() / 2)
}
return r
}
getHeader() {
if (this.isEmpty()) {
throw Error('Queue is empty')
}
return this.queue[this.first]
}
getLength() {
return this.queue.length - 1
}
isEmpty() {
return this.first === this.last
}
resize(length) {
let q = new Array(length)
for (let i = 0; i < length; i++) {
q[i] = this.queue[(i + this.first) % this.queue.length]
}
this.queue = q
this.first = 0
this.last = this.size
}
}
链表:是一个线性结构,同时也是一个天然的递归结构。链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大
单向链表:
class Node {
constructor(v, next) {
this.value = v
this.next = next
}
}
class LinkList {
constructor() {
// 链表长度
this.size = 0
// 虚拟头部
this.dummyNode = new Node(null, null)
}
find(header, index, currentIndex) {
if (index === currentIndex) return header
return this.find(header.next, index, currentIndex + 1)
}
addNode(v, index) {
this.checkIndex(index)
// 当往链表末尾插入时,prev.next 为空
// 其他情况时,因为要插入节点,所以插入的节点
// 的 next 应该是 prev.next
// 然后设置 prev.next 为插入的节点
let prev = this.find(this.dummyNode, index, 0)
prev.next = new Node(v, prev.next)
this.size++
return prev.next
}
insertNode(v, index) {
return this.addNode(v, index)
}
addToFirst(v) {
return this.addNode(v, 0)
}
addToLast(v) {
return this.addNode(v, this.size)
}
removeNode(index, isLast) {
this.checkIndex(index)
index = isLast ? index - 1 : index
let prev = this.find(this.dummyNode, index, 0)
let node = prev.next
prev.next = node.next
node.next = null
this.size--
return node
}
removeFirstNode() {
return this.removeNode(0)
}
removeLastNode() {
return this.removeNode(this.size, true)
}
checkIndex(index) {
if (index < 0 || index > this.size) throw Error('Index error')
}
getNode(index) {
this.checkIndex(index)
if (this.isEmpty()) return
return this.find(this.dummyNode, index, 0).next
}
isEmpty() {
return this.size === 0
}
getSize() {
return this.size
}
}
树:
二叉树:天然的递归结构,拥有一个根节点,每个节点至多拥有两个子节点,分别为:左节点和右节点。树的最底部节点称之为叶节点,当一颗树的叶数量为满时,该树可以称之为满二叉树。
二分搜索树:二分搜索树也是二叉树,拥有二叉树的特性。但是区别在于二分搜索树每个节点的值都比他的左子树的值大,比右子树的值小。这种存储方式很适合于数据搜索。
AVL 树:AVL 树改进了二分搜索树,在 AVL 树中任意节点的左右子树的高度差都不大于 1,这样保证了时间复杂度是严格的 O(logN)。基于此,对 AVL 树增加或删除节点时可能需要旋转树来达到高度的平衡。
Trie:又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。
并查集:一种特殊的树结构,用于处理一些不交集的合并及查询问题。该结构中每个节点都有一个父节点,如果只有当前一个节点,那么该节点的父节点指向自己。
Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
Union:将两个子集合并成同一个集合。
堆:通常是一个可以被看做一棵树的数组对象。堆的实现通过构造二叉堆,实为二叉树的一种。
* 算法
按位操作
按位与
按位或
按位异或
排序
冒泡排序
function bubble(array) {
checkArray(array);
for (let i = array.length - 1; i > 0; i--) {
// 从 0 到 `length - 1` 遍历
for (let j = 0; j < i; j++) {
if (array[j] > array[j + 1]) swap(array, j, j + 1)
}
}
return array;
}
插入排序
function insertion(array) {
if (!checkArray(array)) return
for (let i = 1; i < array.length; i++) {
for (let j = i - 1; j >= 0 && array[j] > array[j + 1]; j--)
swap(array, j, j + 1);
}
return array;
}
选择排序
function selection(array) {
if (!checkArray(array)) return
for (let i = 0; i < array.length - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < array.length; j++) {
minIndex = array[j] < array[minIndex] ? j : minIndex;
}
swap(array, i, minIndex);
}
return array;
}
归并排序
function sort(array) {
if (!checkArray(array)) return
mergeSort(array, 0, array.length - 1);
return array;
}
function mergeSort(array, left, right) {
// 左右索引相同说明已经只有一个数
if (left === right) return;
// 等同于 `left + (right - left) / 2`
// 相比 `(left + right) / 2` 来说更加安全,不会溢出
// 使用位运算是因为位运算比四则运算快
let mid = parseInt(left + ((right - left) >> 1));
mergeSort(array, left, mid);
mergeSort(array, mid + 1, right);

let help = [];
let i = 0;
let p1 = left;
let p2 = mid + 1;
while (p1 <= mid && p2 <= right) {
help[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];
}
while (p1 <= mid) {
help[i++] = array[p1++];
}
while (p2 <= right) {
help[i++] = array[p2++];
}
for (let i = 0; i < help.length; i++) {
array[left + i] = help[i];
}
return array;
}
快速排序
function sort(array) {
if (!checkArray(array)) return
quickSort(array, 0, array.length - 1);
return array;
}
function quickSort(array, left, right) {
if (left < right) {
swap(array, , right)
// 随机取值,然后和末尾交换,这样做比固定取一个位置的复杂度略低
let indexs = part(array, parseInt(Math.random() * (right - left + 1)) + left, right);
quickSort(array, left, indexs[0]);
quickSort(array, indexs[1] + 1, right);
}
}
function part(array, left, right) {
let less = left - 1;
let more = right;
while (left < more) {
if (array[left] < array[right]) {
// 当前值比基准值小,`less` 和 `left` 都加一
++less;
++left;
} else if (array[left] > array[right]) {
// 当前值比基准值大,将当前值和右边的值交换
// 并且不改变 `left`,因为当前换过来的值还没有判断过大小
swap(array, --more, left);
} else {
// 和基准值相同,只移动下标
left++;
}
}
// 将基准值和比基准值大的第一个值交换位置
// 这样数组就变成 `[比基准值小, 基准值, 比基准值大]`
swap(array, right, more);
return [less, more];
}
堆排序
堆排序利用了二叉堆的特性来做,二叉堆通常用数组表示,并且二叉堆是一颗完全二叉树(所有叶节点(最底层的节点)都是从左往右顺序排序,并且其他层的节点都是满的)。二叉堆又分为大根堆与小根堆。
1.大根堆是某个节点的所有子节点的值都比他小
2.小根堆是某个节点的所有子节点的值都比他大
堆排序的原理就是组成一个大根堆或者小根堆。以小根堆为例,某个节点的左边子节点索引是 i * 2 + 1,右边是 i * 2 + 2,父节点是 (i - 1) /2。
1.首先遍历数组,判断该节点的父节点是否比他小,如果小就交换位置并继续判断,直到他的父节点比他大
2.重新以上操作 1,直到数组首位是最大值
3.然后将首位和末尾交换位置并将数组长度减一,表示数组末尾已是最大值,不需要再比较大小
4.对比左右节点哪个大,然后记住大的节点的索引并且和父节点对比大小,如果子节点大就交换位置
5.重复以上操作 3 - 4 直到整个数组都是大根堆。
function heap(array) {
if (!checkArray(array)) return
// 将最大值交换到首位
for (let i = 0; i < array.length; i++) {
heapInsert(array, i);
}
let size = array.length;
// 交换首位和末尾
swap(array, 0, --size);
while (size > 0) {
heapify(array, 0, size);
swap(array, 0, --size);
}
return array;
}
function heapInsert(array, index) {
// 如果当前节点比父节点大,就交换
while (array[index] > array[parseInt((index - 1) / 2)]) {
swap(array, index, parseInt((index - 1) / 2));
// 将索引变成父节点
index = parseInt((index - 1) / 2);
}
}
function heapify(array, index, size) {
let left = index * 2 + 1;
while (left < size) {
// 判断左右节点大小
let largest =
left + 1 < size && array[left] < array[left + 1] ? left + 1 : left;
// 判断子节点和父节点大小
largest = array[index] < array[largest] ? largest : index;
if (largest === index) break;
swap(array, index, largest);
index = largest;
left = index * 2 + 1;
}
}
链表
反转单向链表
var reverseList = function(head) {
// 判断下变量边界问题
if (!head || !head.next) return head
// 初始设置为空,因为第一个节点反转后就是尾部,尾部节点指向 null
let pre = null
let current = head
let next
// 判断当前节点是否为空
// 不为空就先获取当前节点的下一节点
// 然后把当前节点的 next 设为上一个节点
// 然后把 current 设为下一个节点,pre 设为当前节点
while(current) {
next = current.next
current.next = pre
pre = current
current = next
}
return pre
};

先序遍历表示先访问根节点,然后访问左节点,最后访问右节点。
中序遍历表示先访问左节点,然后访问根节点,最后访问右节点。
后序遍历表示先访问左节点,然后访问右节点,最后访问根节点。
递归实现
function TreeNode(val) {
this.val = val;
this.left = this.right = null;
}
var traversal = function(root) {
if (root) {
// 先序
console.log(root);
traversal(root.left);
// 中序
// console.log(root);
traversal(root.right);
// 后序
// console.log(root);
}
};
非递归实现
非递归实现使用了栈的结构,通过栈的先进后出模拟递归实现。
// 先序遍历代码实现
function pre(root) {
if (root) {
let stack = [];
// 先将根节点 push
stack.push(root);
// 判断栈中是否为空
while (stack.length > 0) {
// 弹出栈顶元素
root = stack.pop();
console.log(root);
// 因为先序遍历是先左后右,栈是先进后出结构
// 所以先 push 右边再 push 左边
if (root.right) {
stack.push(root.right);
}
if (root.left) {
stack.push(root.left);
}
}
}
}
// 中序遍历代码实现
function mid(root) {
if (root) {
let stack = [];
// 中序遍历是先左再根最后右
// 所以首先应该先把最左边节点遍历到底依次 push 进栈
// 当左边没有节点时,就打印栈顶元素,然后寻找右节点
// 对于最左边的叶节点来说,可以把它看成是两个 null 节点的父节点
// 左边打印不出东西就把父节点拿出来打印,然后再看右节点
while (stack.length > 0 || root) {
if (root) {
stack.push(root);
root = root.left;
} else {
root = stack.pop();
console.log(root);
root = root.right;
}
}
}
}
// 后序遍历代码实现
// 该代码使用了两个栈来实现遍历,相比一个栈的遍历来说要容易理解很多
function pos(root) {
if (root) {
let stack1 = [];
let stack2 = [];
// 后序遍历是先左再右最后根
// 所以对于一个栈来说,应该先 push 根节点
// 然后 push 右节点,最后 push 左节点
stack1.push(root);
while (stack1.length > 0) {
root = stack1.pop();
stack2.push(root);
if (root.left) {
stack1.push(root.left);
}
if (root.right) {
stack1.push(root.right);
}
}
while (stack2.length > 0) {
console.log(s2.pop());
}
}
}
树的深度
树的最大深度
var maxDepth = function(root) {
if (!root) return 0
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1
};
动态规划
1.自底向上分解子问题
2.通过变量存储已经计算过的解
function fib(n) {
let array = new Array(n + 1).fill(null)
array[0] = 0
array[1] = 1
for (let i = 2; i <= n; i++) {
array[i] = array[i - 1] + array[i - 2]
}
return array[n]
}
fib(10)
0-1 背包问题
最长递增子序列
* CSS
// https://segmentfault.com/a/1190000013860482
// https://github.com/AllThingsSmitty/css-protips
// https://github.com/chokcoco/iCSS
// https://github.com/30-seconds/30-seconds-of-css
* JS
// https://github.com/getify/Functional-Light-JS
// https://github.com/leonardomso/33-js-concepts
// https://github.com/dt-fe/weekly
// https://github.com/JohnsenZhou/Front-End-Performance-Checklist
// https://github.com/AllThingsSmitty/must-watch-javascript
// https://github.com/30-seconds/30-seconds-of-code
坚持原创技术分享,您的支持将鼓励我继续创作!